#!/usr/bin/env python
# --coding:utf-8--
"""
@Author  : cpl
"""



import logging
from neo4j import GraphDatabase
from neo4j.exceptions import ServiceUnavailable
import json
from npm_utils import npm_VersionUtils
import re
import GlobalVar

class App:
    def __init__(self, uri, user, password):
        self.driver = GraphDatabase.driver(uri, auth=(user, password), max_connection_lifetime=3600 * 24 * 30,
                                           keep_alive=True)
        self.verutils = npm_VersionUtils()

        # self.license_set1 = set(['AGPL-3.0'])
        # self.license_set2 = set(['GPL-2.0', 'GPL-3.0', 'EPL-2.0', 'MPL-2.0'])
        # self.license_set3 = set(['LGPL-3.0', 'LGPL-2.1'])
        # self.license_set4 = set(['Apache-2.0', 'CC0-1.0', 'BSD-3-Clause', 'BSD-2-Clause', 'MIT'])
        self.license_set = {'UNLICENSED': 0, 'Unlicense': 0,
                            'AGPL-3.0': 5,
                            'GPL-2.0': 4, 'GPL-3.0': 4, 'EPL-2.0': 4, 'MPL-2.0': 4,
                            'LGPL-3.0': 3, 'LGPL-2.1': 3,
                            'Apache-2.0': 2, 'CC0-1.0': 2, 'BSD-3-Clause': 2, 'BSD-2-Clause': 2, 'MIT': 2,
                            'Apache2': 2,
                            'Plubic Domain': 1
                            }

    def close(self):
        # Don't forget to close the driver connection when you are finished with it
        self.driver.close()


    # 生成 name 所有版本的节点，并将其加入待构建队列
    def load_allversion(self, tx, pkg_json, name, st = 0, ed = 0):
        if 'versions' in pkg_json.keys():
            if st == 0 and ed == 0:
                data = pkg_json['versions'].items()
            else:
                data = list(pkg_json['versions'].items())[st:ed]
            for cur_version, ver_info in data:
                self._create_basic(tx, pkg_json, name, cur_version)
                #self.load_pkg(tx, pkg_json, name, cur_version)
                GlobalVar.add_ordernum()
                cur_id = ver_info['_id'] if '_id' in ver_info.keys() else name + '@' + cur_version
                if GlobalVar.not_seen(cur_id):
                 # 不在队列里，继续检查数据库，判断曾经是否入队
                    #if not self.find_pkg(cur_id, name):
                    if not self.find_pkg(cur_id):
                        #print('load all: ',name+'@'+cur_version+'\n')
                        GlobalVar.add_queue(name, cur_version)
                        GlobalVar.add_seen(cur_id)
            # 事实上在session.begin_transaction()里，没有commit这些操作都没有完成
            tx.run("MERGE (p1:Pkg{ name: $name, id: $id, allv: True}) RETURN p1.id", name=name, id=name+'@'+'all')
            GlobalVar.add_seenall(name)
            #tx.commit()
        else:
            self._create_basic(tx, pkg_json, name, '')
            GlobalVar.add_ordernum()
            GlobalVar.add_seenall(name)



    #生成 name@version 的维护者和依赖项信息，并将依赖项加入待构建队列
    def load_pkg(self, tx, pkg_json, name, version):
        # if name == 'ghost' and version == '2.28.0':
        #     print("init load pkg %s@%s" % (name, version))
        if version == 'INIT':
            return
        pkg_name = name
        pkg_verson = version
        if version == None or version == '':
            # 找不到符合约束的最大版本，或者没有'versions'字段
            version = ''
            pkg_id = pkg_name
        else:
            pkg_id = pkg_name + '@' + version

        self._create_basic(tx, pkg_json, name, version)

        

        GlobalVar.add_ordernum()
        #检查是否有’maintainers‘, 'dependency'字段
        maintainers = None
        dependency = None

        

        if 'versions' in pkg_json.keys() and version in pkg_json['versions'].keys():
            if 'maintainers' in pkg_json['versions'][version].keys():
                maintainers = pkg_json['versions'][version]['maintainers']
            elif 'maintainers' in pkg_json.keys():
                maintainers = pkg_json['maintainers']
                # print("-%s %s- has no 'maintainers', use the outside 'maintainers' !"%(name, version))
            else:
                pass

            if 'dependencies' in pkg_json['versions'][version].keys():
                dependency = pkg_json['versions'][version]['dependencies']
            elif 'dependencies' in pkg_json.keys():
                dependency = pkg_json['dependencies']
                # print("-%s %s- has no 'dependencies', use the outside 'dependencies' !" % (name, version))
            else:
                pass

        else:
            if 'maintainers' in pkg_json.keys():
                maintainers = pkg_json['maintainers']
                # print("-%s- has no 'versions' or version -%s-, use the outside 'maintainers' !"%(name, version))
            if 'dependencies' in pkg_json.keys():
                dependency = pkg_json['dependencies']
                # print("-%s- has no 'versions' or version -%s-, use the outside 'dependencies' !" % (name, version))



        if maintainers != None:
            # 约定API中’maintainers'字段为dict的list
            for cur_maintainer in maintainers:
                #owner_rlt = session.write_transaction(self._create_owner, pkg_id, cur_maintainer)
                self._create_owner(tx, pkg_id, cur_maintainer)
                GlobalVar.add_ordernum()
        else:
            # print("-%s- has no 'maintainers' !" % (name))
            pass


        if dependency != None:
            #约定API中’dependencies'字段为dict

            for name, version in dependency.items():
                self.verutils.set_name_version(name, version)
                if 'error' in self.verutils.load_pkgjson():
                    continue

                max_version = self.verutils.max_satisfied()
                if (max_version == None):
                    max_version = ''
                    
                dependency_id = name if max_version == '' else name + '@' + max_version

                self._create_dependency(tx, pkg_id, dependency_id)
                GlobalVar.add_ordernum()
                # print('get dependency: %s %s'%(name, max_version))
                if  GlobalVar.not_seen(dependency_id):
                    # 不在队列里，继续检查数据库，判断曾经是否入队
                    if not self.find_pkg(dependency_id):
                        # print(name+'-v'+version+'\n')
                        GlobalVar.add_queue(name, max_version)
                        GlobalVar.add_seen(dependency_id)

        # 没有依赖项，使用属性标记叶节点
        else:
            tx.run("MERGE (p1:Pkg{id: $id}) SET p1.isleaf = $leaf_flag RETURN p1.id", id=pkg_id, leaf_flag=True)

        # if pkg_name == 'ghost' and pkg_verson == '2.28.0':
        #     print("finish load pkg %s@%s" % (pkg_name, pkg_verson))

    def _create_basic(self, tx, pkg_json, pkg_name, version):
        if version == '':
            pkg_id = pkg_name
            pkg_time = pkg_json['time']['created'] if 'time' in pkg_json.keys() and 'created' in pkg_json[
                'time'].keys() else 'Unknown'
        else:
            pkg_id = pkg_name + '@' + version
            pkg_time = pkg_json['time'][version] if 'time' in pkg_json.keys() and version in pkg_json[
                'time'].keys() else 'Unknown'

        ver_info = pkg_json
        if 'versions' in pkg_json.keys() and version in pkg_json['versions'].keys():
            ver_info = pkg_json['versions'][version]
            pkg_id = ver_info['_id'] if '_id' in ver_info.keys() else pkg_name + '@' + version

        pkg_license, license_level = self.handle_license(ver_info, version)


        pkg_description = ver_info['description'] if 'description' in ver_info.keys() else 'None'
        pkg_homepage = ver_info['homepage'] if 'homepage' in ver_info.keys() else 'Unknown'

        query = (
            "MERGE (p1:Pkg{ id: $pkg_id }) SET p1.name = $pkg_name, p1.version = $version, p1.time = $pkg_time, p1.description = $pkg_description, p1.license = $pkg_license, p1.homepage = $pkg_homepage "
            "MERGE (p2: License{name: $pkg_license, level: $license_level}) "
            "MERGE (p1)-[use:Use]->(p2) "
            "RETURN p1"
        )
        result = tx.run(query, pkg_id=pkg_id, pkg_name=pkg_name, version=version, pkg_time=pkg_time,
                        pkg_description=pkg_description, pkg_license=pkg_license, pkg_homepage=pkg_homepage,
                        license_level=license_level)
        try:
            return [{"pkg ": record["p1"]["id"]}
                    for record in result]
        # Capture any errors along with the query and data for traceability
        except ServiceUnavailable as exception:
            logging.error("CREATE BASIC- {query} raised an error: \n {exception}".format(
                query=query, exception=exception))
            raise

    def search_license_level(self, license):
        if license in self.license_set.keys():
            return self.license_set[license]
        else:
            return 1

    def find_maxl(self, l):
        max_level = -1
        for license in l:
            cur_level = self.search_license_level(license)
            if cur_level > max_level:
                max_level = cur_level
                max_license = license
        return max_license, max_level

    def find_minl(self, l):
        min_level = 10
        for license in l:
            cur_level = self.search_license_level(license)
            if cur_level < min_level:
                min_level = cur_level
                min_license = license
        return min_license, min_level

    def single_license(self, license_str):
        # print(type(license_str))
        # if isinstance(license_str, list):
        #     print(license_str)

        if isinstance(license_str, dict):
            if 'type' in license_str.keys():
                license = license_str['type']
            elif 'name' in license_str.keys():
                license = license_str['name']
            else:
                license = 'UNKNOWN'
            return license, self.search_license_level(license)
        else:
            if 'SEE LICENSE IN' in license_str:
                return 'UNKNOWN', self.search_license_level('UNKNOWN')
            elif 'with' in license_str:
                license = re.split(r'-with-', license_str)[0]
                if license == '' or license == None:
                    license = 'UNKNOWN'
                return license, self.search_license_level(license)
            elif 'License, Version' in license_str:
                str_ = re.split(r'License, Version', license_str)
                license = str_[0].strip()
                if len(str_) >= 2:
                    license += '-' + str_[1].strip()
                return license, self.search_license_level(license)
            elif '+' in license_str:
                license = license_str.rstrip('+')
                m = re.match(r"^([a-zA-Z]+)([0-9]+\.[0-9]+)$", license)
                str = 'UNKNOWN'
                if m != None:
                    str = m.group(1) + '-' + m.group(2)
                return license, self.search_license_level(str)
            else:
                if license_str == None:
                    return 'UNKNOWN', self.search_license_level('UNKNOWN')
                license = license_str.strip("(")
                license = license.strip(")")
                l = license.replace(" ", "")
                l = l.replace('or', 'OR')
                l = l.replace('Or', 'OR')
                l = l.replace('and', 'AND')
                l = l.replace('And', 'AND')
                if 'OR' in l:
                    l = re.split(r'OR', l)
                    min_license, min_level = self.find_minl(l)
                    return license, min_level
                elif 'AND' in l:
                    l = re.split(r'AND', l)
                    max_license, max_level = self.find_maxl(l)
                    return license, max_level
                elif '/' in l:
                    l = re.split(r'/', l)
                    min_license, min_level = self.find_minl(l)
                    return license, min_level
                else:
                    license = '' if license_str == None else license_str
                    return license, self.search_license_level(license)

    def handle_license(self, license_info, version):
        if 'license' in license_info.keys():
            if isinstance(license_info['license'], list):
                # 佛了，license字段还可能是list, list里还可能是dict
                max_level = -1
                max_license = ''
                for license in license_info['license']:
                    cur_license, cur_level = self.single_license(license)
                    if cur_level > max_level:
                        max_level = cur_level
                        max_license = cur_license
                return  max_license, max_level
            else:
                return self.single_license(license_info['license'])

        elif 'licenses' in license_info.keys():
            max_level = -1
            max_license = ''
            for license in license_info['licenses']:
                if isinstance(license, list):
                    for sub_license in license:
                        cur_license, cur_level = self.single_license(sub_license)
                        if cur_level > max_level:
                            max_level = cur_level
                            max_license = cur_license
                else:
                    cur_license, cur_level = self.single_license(license)
                    if cur_level > max_level:
                        max_level = cur_level
                        max_license = cur_license
            return max_license, max_level
        else:
            return 'UNDECLARED', 0

    @staticmethod
    def _create_owner(tx, pkg_id, cur_maintainer):
        if isinstance(cur_maintainer, str):
            cur_maintainer = re.match(r"^([a-zA-Z\s\-]+)<(.*\@.*)>.*", cur_maintainer)
            if cur_maintainer != None:
                name = cur_maintainer.group(1)
                email = cur_maintainer.group(2)
            else:
                name = 'unknown'
                email = 'unknown'
                return None
        elif isinstance(cur_maintainer, dict):
            if 'name' not in cur_maintainer:
                # print("-%s- cannot resolve maintainer: %s"%(pkg_id, cur_maintainer))
                return None
            name = cur_maintainer['name']
            email = cur_maintainer['email'] if 'email' in cur_maintainer else 'unknown'
        else:
            name = 'unknown'
            email = 'unknown'
            return None

        query = (
            "MATCH (p1:Pkg{id:$pkg_id}) "
            "MERGE (p2:Maintainer{ name: $name, email:$email}) "
            "MERGE (p1)-[owned:Maintainedby]->(p2) "
            "RETURN p1,p2"
        )
        result = tx.run(query, pkg_id=pkg_id, name=name, email=email)
        try:
            return [{"pkg": record["p1"]["id"], 'maintainer': record["p2"]["name"] + ' ' + record["p2"]["email"]}
                    for record in result]
        # Capture any errors along with the query and data for traceability
        except ServiceUnavailable as exception:
            logging.error("CREATE maintainer- {query} raised an error: \n {exception}".format(
                uery=query, exception=exception))
            raise

    @staticmethod
    def _create_dependency(tx, pkg_id, dependency_id):
        # dependency_id = name if version == '' else name + '-v' + version
        query = (
            "MATCH (p1:Pkg{id:$pkg_id}) "
            "MERGE(p2:Pkg{id:$dependency_id}) "
            "MERGE (p1)-[depend:DependOn]->(p2) "
            "RETURN p1,p2"
        )
        result = tx.run(query, pkg_id=pkg_id, dependency_id=dependency_id)
        try:
            return [{"pkg": record["p1"]["id"], 'dependcy': record["p2"]["id"]}
                    for record in result]
        # Capture any errors along with the query and data for traceability
        except ServiceUnavailable as exception:
            logging.error("CREATE DEPENDENCY- {query} raised an error: \n {exception}".format(
                uery=query, exception=exception))
            raise

    #判断是否构建了依赖
    def find_pkg(self, pkg_id):
        with self.driver.session() as session:
            result = session.read_transaction(self._find_and_return_pkg, pkg_id)
            return result

    @staticmethod
    def _find_and_return_pkg(tx, pkg_id):
        query = (
            "MATCH(p:Pkg{id:$pkg_id})-[:DependOn*1]->(q:Pkg) "
            "RETURN p.id as id "
            "UNION "
            "MATCH(x:Pkg{id:$pkg_id}) where x.isleaf = True "
            "RETURN x.id as id "
        )
        result = tx.run(query, pkg_id=pkg_id)
        return [record["id"] for record in result]

    def find_sublicenses(self, pkg_id):
        with self.driver.session() as session:
            result = session.read_transaction(self._find_and_return_sublicenses, pkg_id)
            return result

    @staticmethod
    def _find_and_return_sublicenses(tx, pkg_id):
        query = (
            "MATCH (p:Pkg {id: $pkg_id}) "
            "CALL apoc.path.subgraphNodes(p, { relationshipFilter: 'DependOn>', minLevel:1}) "
            "YIELD node "
            "WITH node.license AS license, count(node) as count "
            "RETURN license, count"
        )
        result = tx.run(query, pkg_id=pkg_id)
        return [{record['license']: record['count']} for record in result]
